package io.gitee.loulan_yxq.owner.mybatisPlus.bean;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.gitee.loulan_yxq.owner.core.collection.ArrayTool;
import io.gitee.loulan_yxq.owner.core.collection.CollTool;
import io.gitee.loulan_yxq.owner.core.map.MapTool;
import io.gitee.loulan_yxq.owner.core.stream.CollectorTool;
import io.gitee.loulan_yxq.owner.core.tool.AssertTool;
import io.gitee.loulan_yxq.owner.core.tool.ObjectTool;
import io.gitee.loulan_yxq.owner.core.tool.ReflectTool;
import io.gitee.loulan_yxq.owner.core.tool.StrTool;
import io.gitee.loulan_yxq.owner.mybatisPlus.exception.SQLRuntimeException;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.PreparedStatementHandler;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.*;
import java.util.List;
import java.util.Map;

/*********************************************************
 ** 可以直接执行的sql类
 ** <br><br>
 ** Date: Created in 2022/8/22 11:17
 ** @author loulan
 ** @version 0.0.0
 *********************************************************/
public class ExecuteSQL {

    // 数据源连接
    private Connection conn;

    /**
     * 将构造方法私有化，进行直接进行对象的构造
     *
     * @param conn 数据源连接对象
     * @author :loulan
     */
    private ExecuteSQL(Connection conn) {
        this.conn = conn;
    }

    /**
     * 实例化工具类
     *
     * @param conn 数据源连接对象
     * @return ExecuteSQL对象
     * @author :loulan
     */
    public static ExecuteSQL instance(Connection conn){
        AssertTool.notNull(conn, "数据库连接对象不能为空。");
        return new ExecuteSQL(conn);
    }

    /**
     * 执行select查询语句。(#{}表示参数占位，${}表示sql注入)。如果占位符找不到对应的数据那么直接抛出异常
     *
     * @param isCamel 是否将下划线转换为驼峰式
     * @param sql     要进行执行的sql语句,占位符使用 #{}和${}
     * @param obj     占位符所替换的参数,可以为实体类对象，也可以是Map集合。(参数可以比占位符多，但是不能少)
     * @return 查询到的数据
     * @author :loulan
     */
    public List<Map<String, Object>> getData(String sql, Object obj, boolean isCamel) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(obj, sql);

            // 执行查询
            ResultSet resultSet = preparedStatement.executeQuery();

            return resultSetHandler(resultSet, isCamel);
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 执行查询语句。(参数占位符请使用?(英文)占位)
     *
     * @param isCamel 是否将下划线转换为驼峰式
     * @param sql     要进行执行的sql语句,占位符使用?
     * @param params  占位符所替换的参数，按顺序进行替换，请确保替换的参数数据类型和数据库数据类型可进行匹配。(参数可以比占位符多，但是不能少)
     * @return 查询到的数据
     * @author :loulan
     */
    public List<Map<String, Object>> getData(String sql, boolean isCamel, Object... params) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(sql, params);

            // 执行查询
            ResultSet resultSet = preparedStatement.executeQuery();

            return resultSetHandler(resultSet, isCamel);
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 执行select查询语句。(#{}表示参数占位，${}表示sql注入)。如果占位符找不到对应的数据那么直接抛出异常
     *
     * @param sql  要进行执行的sql语句,占位符使用 #{}和${}
     * @param obj  占位符所替换的参数,可以为实体类对象，也可以是Map集合。(参数可以比占位符多，但是不能少)
     * @param clzz 获取数据对应的实体类对象
     * @return 查询到的数据
     * @author :loulan
     */
    public <T> List<T> getData(String sql, Object obj, Class<T> clzz) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(obj, sql);

            // 执行查询
            ResultSet resultSet = preparedStatement.executeQuery();

            return resultSetHandler(resultSet, clzz);
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        } catch (InstantiationException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 执行查询语句。(参数占位符请使用?(英文)占位)
     *
     * @param sql    要进行执行的sql语句,占位符使用?
     * @param clzz   获取数据对应的实体类对象
     * @param params 占位符所替换的参数，按顺序进行替换，请确保替换的参数数据类型和数据库数据类型可进行匹配。(参数可以比占位符多，但是不能少)
     * @return 查询到的数据
     * @author :loulan
     */
    public <T> List<T> getData(String sql, Class<T> clzz, Object... params) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(sql, params);

            // 执行查询
            ResultSet resultSet = preparedStatement.executeQuery();

            return resultSetHandler(resultSet, clzz);
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        } catch (InstantiationException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 执行修改语句，包括插入，更新，删除等语句。(参数占位符请使用?(英文)占位)
     *
     * @param sql    要进行执行的sql语句,占位符使用?
     * @param params 占位符所替换的参数，按顺序进行替换，请确保替换的参数数据类型和数据库数据类型可进行匹配。(参数可以比占位符多，但是不能少)
     * @return 修改数据的条数
     * @author :loulan
     */
    public int fixData(String sql, Object... params) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(sql, params);
            int result = preparedStatement.executeUpdate();
            return result;
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 执行修改语句，包括插入，更新，删除等语句。(#{}表示参数占位，${}表示sql注入)。如果占位符找不到对应的数据那么直接抛出异常
     *
     * @param obj 占位符所替换的参数,可以为实体类对象，也可以是Map集合。(参数可以比占位符多，但是不能少)
     * @param sql 要进行执行的sql语句,占位符使用 #{}和${}
     * @return 修改数据的条数
     * @author :loulan
     */
    public int fixData(Object obj, String sql) {
        try {
            PreparedStatement preparedStatement = getPreparedStatement(obj, sql);
            int result = preparedStatement.executeUpdate();
            return result;
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 关闭连接
     *
     * @author :loulan
     */
    public void close() {
        AssertTool.notNull(conn, "数据源连接不能为空。");
        try {
            if (!conn.isClosed()) {
                conn.close();
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 是否开启自动提交功能（默认就是自动提交功能）
     *
     * @param isAuto true-开启自动提交功能，false-关闭自动提交改为手动提交功能
     * @author :loulan
     */
    public void autoCommit(boolean isAuto) {
        try {
            conn.setAutoCommit(isAuto);
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 进行事务的提交
     *
     * @author :loulan
     */
    public void commit() {
        try {
            conn.commit();
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 进行事务的回滚
     *
     * @author :loulan
     */
    public void rollback() {
        try {
            conn.rollback();
        } catch (SQLException ex) {
            ex.printStackTrace();
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 获取{@link PreparedStatement}对象(#{}表示参数占位，${}表示sql注入)。如果占位符找不到对应的数据那么直接抛出异常
     *
     * @param sql 要进行执行的sql语句,占位符使用 #{}和${}
     * @param obj 占位符所替换的参数,可以为实体类对象，也可以是Map集合。(参数可以比占位符多，但是不能少)
     * @return PreparedStatement可执行对象
     * @throws SQLException sql异常
     * @author :loulan
     */
    private PreparedStatement getPreparedStatement(Object obj, String sql) throws SQLException {
        AssertTool.notEmpty(sql, "sql语句不能为空。");

        Map<String, Object> paramMap = null;
        if (ObjectTool.isNotNull(obj) && ObjectTool.isInstanceof(obj, Map.class)) {
            paramMap = (Map<String, Object>) obj;
        }

        // 要注入的sql
        List<String> sqlNames = StrTool.regexStrs(sql, "\\$\\{(\\s*\\S+\\s*)\\}", 1);
        // sql的注入直接进行拼接替换
        if (CollTool.isNotEmpty(sqlNames)) {
            AssertTool.notNull(obj, "参数不能为空");
            // 进行遍历替换
            for (String sqlName : sqlNames) {
                Object o = ObjectTool.isNotNull(paramMap) ? paramMap.get(sqlName.trim()) : ReflectTool.getFieldValue(obj, sqlName.trim());
                // 判断数据必须存在，否则抛出异常，而且这个数据必须是String类型的
                AssertTool.notNull(o, sqlName + "参数的数据不存在。");
                // 替换条所有的注入sql
                sql = sql.replaceAll("\\$\\{\\s*" + sqlName.trim() + "\\s*\\}", String.valueOf(o));
            }
        }

        // 要占位的参数
        List<String> paramNames = StrTool.regexStrs(sql, "\\#\\{(\\s*\\S+\\s*)\\}", 1);
        List<Object> params = CollTool.list();
        if (CollTool.isNotEmpty(paramNames)) {
            AssertTool.notNull(obj, "参数不能为空");
            // 将所有的参数占位符替换为？占位符
            sql = sql.replaceAll("\\#\\{\\s*\\S+\\s*\\}", "?");
            for (String paramName : paramNames) {
                Object o = ObjectTool.isNotNull(paramMap) ? paramMap.get(paramName.trim()) : ReflectTool.getFieldValue(obj, paramName.trim());
                // 判断数据必须存在，否则抛出异常
                AssertTool.notNull(o, paramName + "参数的数据不存在。");
                params.add(o);
            }
        }
        return getPreparedStatement(sql, params.stream().toArray());
    }

    /**
     * 获取{@link PreparedStatement}对象(sql的占位符是?)
     *
     * @param sql    要进行执行的sql语句,占位符使用?
     * @param params 占位符所替换的参数，按顺序进行替换，请确保替换的参数数据类型和数据库数据类型可进行匹配。(参数可以比占位符多，但是不能少)
     * @return PreparedStatement可执行对象
     * @throws SQLException sql异常
     * @author :loulan
     */
    private PreparedStatement getPreparedStatement(String sql, Object... params) throws SQLException {
        AssertTool.notNull(conn, "数据源连接不能为空。");
        AssertTool.isFalse(conn.isClosed(), "当前数据源连接已经关闭。");
        AssertTool.notEmpty(sql, "sql语句不能为空。");


        PreparedStatement preparedStatement = conn.prepareStatement(sql);

        // 捕获所有的占位符?
        List<String> marks = StrTool.regexStrs(sql, "(\\?)", 1);
        if (CollTool.isNotEmpty(marks)) {
            // 产看确定参数和占位符数量的对比
            AssertTool.notEmpty(params, "参数数量少于占位符数量。");
            AssertTool.isTrue(params.length >= marks.size(), "参数数量少于占位符数量。");

            // 进行占位符替换数据
            for (int i = 1; i <= marks.size(); i++) {
                preparedStatement.setObject(i, params[i - 1]);
            }
        }

        return preparedStatement;
    }

    /**
     * 结果集的处理器，从结果集中获取数据(返回{@link List<Map<String,Object>>})
     *
     * @param resultSet 结果集
     * @param isCamel   是否转换为驼峰是的属性名称
     * @return 集合数据
     * @throws SQLException sql异常
     * @author :loulan
     */
    private List<Map<String, Object>> resultSetHandler(ResultSet resultSet, boolean isCamel) throws SQLException {
        // 查询结果解析
        List<Map<String, Object>> list = CollTool.list();
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            Map<String, Object> map = MapTool.map();
            for (int i = 1; i <= columnCount; i++) {
                map.put(isCamel ? StrTool.underlineToCamel(metaData.getColumnLabel(i)) : metaData.getColumnLabel(i), resultSet.getObject(i));
            }
            list.add(map);
        }

        return list;
    }

    /**
     * 结果集的处理器，从结果集中获取数据(返回指定类型的对象)
     *
     * @param resultSet 结果集
     * @param clzz      数据对象的类型
     * @return 集合数据
     * @throws SQLException sql异常
     * @author :loulan
     */
    private <T> List<T> resultSetHandler(ResultSet resultSet, Class<T> clzz) throws SQLException, InstantiationException, IllegalAccessException {
        // 查询结果解析
        List<T> list = CollTool.list();

        // 获取表字段名（驼峰式的），以及对应的字段
        Map<String, Field> tFieldMap = ArrayTool.asList(ReflectTool.getFields(clzz)).stream()
                // 过滤那些带有TableField注解并且表明exist为false的数据。因为这些数据明确指明和数据表无关
                .filter(o -> o.isAnnotationPresent(TableField.class) ? o.getAnnotation(TableField.class).exist() : true)
                .collect(CollectorTool.toMap(
                        o -> {
                            // 如果存在TableField和TableId注解，判断这两个注解是否存在value值，如果设置了说明该属性重新设置了和表字段的对应关系，如果不存在或没设置返回属性名称
                            if (o.isAnnotationPresent(TableField.class)) {
                                String fieldName = o.getAnnotation(TableField.class).value();
                                return StrTool.isNotEmpty(fieldName) ? StrTool.underlineToCamel(fieldName) : o.getName();
                            } else if (o.isAnnotationPresent(TableId.class)) {
                                String fieldName = o.getAnnotation(TableId.class).value();
                                return StrTool.isNotEmpty(fieldName) ? StrTool.underlineToCamel(fieldName) : o.getName();
                            } else {
                                return o.getName();
                            }
                        },
                        o -> o,
                        // 如果遇到字段名重复的以第一个为准。因为按照反射获取的字段属性顺序，子类自己的排在前面，父类的属性排在后面
                        (o1, o2) -> o1)
                );
        // 循环解析每一条结果数据
        while (resultSet.next()) {
            // 获取当前行的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();

            // 循环每一列数据将数据分析获取出来
            T t = clzz.newInstance();
            for (int i = 1; i <= columnCount; i++) {
                Field field = tFieldMap.get(StrTool.underlineToCamel(metaData.getColumnLabel(i)));
                if (ObjectTool.isNull(field)) {
                    // 如果对象属性中不存在这一列数据，那么也就不取用这一列的数据了
                    continue;
                }

                // 判断是否是基本数据类型 (基本数据类型和一些特殊包装数据类型需要特别处理)
                Class<?> fieldType = field.getType();
                if (!fieldType.isPrimitive()) {
                    // 包装数据类型值判断获取
                    Object value = null;
                    if (String.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getString(i);
                    } else if (BigDecimal.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getBigDecimal(i);
                    } else if (Blob.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getBlob(i);
                    } else if (byte[].class.isAssignableFrom(fieldType)) {
                        value = resultSet.getBytes(i);
                    } else if (fieldType.isArray()) {
                        value = resultSet.getArray(i);
                    }   else if (Object.class.getName().equals(fieldType.getName())) {
                        // Object不能使用isAssignableFrom，因为Object是所有包装类的基类，所以采用类的全限定名称进行判断
                        value = resultSet.getObject(i);
                    } else {
                        // 如果不是以上的包装类型，那么就直接通过这个方法来进行数据的获取
                        value = resultSet.getObject(i, fieldType);
                    }
                    ReflectTool.setFieldValue(t, field, value);
                } else {
                    // 基本数据类型数据值判断获取
                    Object value = null;
                    if (int.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getInt(i);
                    } else if (double.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getDouble(i);
                    } else if (float.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getFloat(i);
                    } else if (long.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getLong(i);
                    } else if (byte.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getByte(i);
                    } else if (boolean.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getBoolean(i);
                    } else if (short.class.isAssignableFrom(fieldType)) {
                        value = resultSet.getShort(i);
                    } else if (char.class.isAssignableFrom(fieldType)) {
                        String tempValue = resultSet.getString(i);
                        value = StrTool.isNotEmpty(tempValue) ? tempValue.charAt(0) : ' ';
                    }
                    ReflectTool.setFieldValue(t, field, value);
                }
            }
            list.add(t);
        }

        return list;
    }
}
